﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Xml;
using System.Text.RegularExpressions;

namespace BinaryXmlInspector
{
    /// <summary>
    /// Ultra ghetto XML tokenizer.  Should be used for syntax highlighting and other trivial purposes only.
    /// </summary>
    public class XmlTokenizer
    {
        List<Token> tokens = new List<Token>();
        StringIterator reader;        

        public List<Token> Tokenize(string xml)
        {
            tokens.Clear();
            reader = new StringIterator(xml);

            try
            {
                Root();
            }
            catch (XmlException ex)
            {
                // just give up - log the exception, write the rest of the stream as one big token
                Trace.TraceError(ex.ToString());
                if (!reader.EOF)
                {
                    ParseWhile(TokenType.InnerText, c => true);
                }
            }

            return tokens;
        }

        #region Recursive Descent

        private void Root()
        {
            ParseWhiteSpace();            
            switch (reader.Current)
            {
                case null:
                    break;
                case '<':
                    Element();
                    break;
                default:
                    Throw();
                    break;
            }
        }
                
        private void Element()
        {            
            ParseOne(TokenType.ElementBoundary, '<');
            switch (reader.Current)
            {
                case '!':
                    ParseUntilToken(TokenType.Comment, "-->", TokenType.ElementBoundary);
                    break;
                case '?':
                    ParseOne(TokenType.ElementBoundary, '?');
                    ParseUntilToken(TokenType.Comment, "?>", TokenType.ElementBoundary);  // cop out
                    break;
                case '/':
                    ParseOne(TokenType.ElementBoundary, '/');
                    ElementBody();
                    break;
                default:
                    ElementBody();
                    break;
            }

            ParseUntil(TokenType.InnerText, c => c == '<');

            if (reader.EOF)
                return;
            else
                Element();
        }

        private void ElementBody()
        {
            ParseWhile(TokenType.ElementName, c => Char.IsLetterOrDigit(c) || ":_.-".Contains(c.ToString()) );
            AttributeCollection();
        }

        private void AttributeCollection()
        {
            ParseWhiteSpace();
            switch (reader.Current)
            {
                case null:
                    ThrowEOF();
                    break;
                case '/':
                    ParseTwo(TokenType.ElementBoundary, "/>");                    
                    break;
                case '>':
                    ParseOne(TokenType.ElementBoundary, '>');
                    break;
                default:
                    if (!char.IsLetter(reader.Current.Value))
                        Throw();
                    
                    Attribute();
                    AttributeCollection();  // hopefully this counts as tail recursion
                    break;
            }
        }

        private void Attribute()
        {            
            ParseUntil(TokenType.AttributeName, c => char.IsWhiteSpace(c) || c == '=');
            ParseWhiteSpace();
            ParseOne(TokenType.AttributeEquals);
            AttributeValue();
        }

        private void AttributeValue()
        {
            ParseWhiteSpace();
            ParseOne(TokenType.AttributeBoundary);
            ParseUntilToken(TokenType.AttributeValue, "\"", TokenType.AttributeBoundary);
            ParseWhiteSpace();
        }

        #endregion

        #region Helpers
        private void ParseUntil(TokenType type, Func<char, bool> stopCondition)
        {
            ParseWhile(type, c => !stopCondition(c));
        }

        private void ParseWhile(TokenType type, Func<char, bool> whileCondition)
        {
            int oldIndex = reader.Index;
            var value = reader.ReadWhile(whileCondition);
            if (value != "")
                tokens.Add(new Token(type, value, oldIndex, reader.Index));            
        }

        private void ParseWhiteSpace()
        {
            ParseWhile(TokenType.Whitespace, c => char.IsWhiteSpace(c));
            reader.ReadWhile(c => char.IsWhiteSpace(c));
        }

        private void ParseOne(TokenType type)
        {            
            ParseOne(type, Convert.ToChar(type));
        }

        private void ParseOne(TokenType type, char expectedChar)
        {
            if (expectedChar != reader.Current)
                Throw();
            tokens.Add(new Token(type, reader.Current.ToString(), reader.Index, reader.Index));
            reader.MoveNext();
        }

        private void ParseTwo(TokenType type, string expectedString)
        {
            var two = reader.Read(2);
            if (expectedString != null && expectedString != two)
                Throw(two);
            tokens.Add(new Token(type, two, reader.Index - 2, reader.Index - 1));
        }

        private void ParseUntilToken(TokenType curType, string endToken, TokenType nextType)
        {
            var oldIndex = reader.Index;
            var token = reader.ReadUntil(endToken);
            if (token == null)
                ThrowEOF();
            
            var boundary = reader.Index - endToken.Length;

            tokens.Add(new Token(curType, token, oldIndex, boundary));
            tokens.Add(new Token(nextType, endToken, boundary + 1, reader.Index));
        }

        private XmlException ExceptionBuilder(string message, string curToken)
        {
            var ex = new XmlException(String.Format(message, reader.Index));
            ex.Data.Add("BadToken", curToken);
            ex.Data.Add("Index", reader.Index);
            ex.Data.Add("EOF", reader.EOF);
            return ex;
        }

        private void Throw(string curToken)
        {
            throw ExceptionBuilder("Parse exception at character {0}", curToken);
        }

        private void Throw()
        {
            Throw("");
        }

        private void ThrowEOF()
        {
            if (!reader.EOF)
                Throw();        // WTF

            throw ExceptionBuilder("Unexpected EOF at character {0}", "");
        }

        #endregion
    }
}
